package jmine.tec.utils.cripto;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Key;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.List;

import jmine.tec.utils.cripto.exception.CriptoException;
import jmine.tec.utils.cripto.key.GeradorChaveRSA;
import jmine.tec.utils.io.IOUtils;

import org.apache.commons.codec.binary.Base64;

/**
 * Classe que auxilia na utilização de tokens de acesso.
 * 
 * @author cesar olmos
 */
public final class TokenUtil {

    private static final long PRIME_1 = 968050087;

    private static final long PRIME_2 = 555137173;

    private static final int KEY_SIZE = 1024;

    private static final int BLOCK_SIZE = 128;

    private static final String UTF_8 = "UTF-8";

    /**
     * construtor privado.
     */
    private TokenUtil() {
        super();
    }

    /**
     * Gera uma tupla contendo no head a chave púiblic e no tail a chave privada.
     * 
     * @param privateKeyFile privateKeyFile
     * @param publicKeyFile publicKeyFile
     * @param salt salt
     */
    public static void geraChaves(String privateKeyFile, String publicKeyFile, String salt) {
        KeyPair keyPair = GeradorChaveRSA.geraParChavesRSAComSeed(KEY_SIZE, createSeed(salt));
        storeKeyIntoFile(keyPair.getPrivate(), privateKeyFile);
        storeKeyIntoFile(keyPair.getPublic(), publicKeyFile);
    }

    /**
     * Cria um seed para geração das chaves
     * 
     * @param salt salt
     * @return seed
     */
    private static long createSeed(String salt) {
        long seed = PRIME_1;
        seed += seed * PRIME_2 + Long.valueOf(System.currentTimeMillis()).hashCode();
        seed += seed * PRIME_2 + salt.hashCode();
        return seed;
    }

    /**
     * Criptografa os dados utilizando uma chave privada.
     * 
     * @param publicKeyFile a chave privada.
     * @param data os dados a serem criptografados.
     * @return os dados criptografados.
     */
    public static String encripta(String publicKeyFile, String data) {
        PublicKey key;
        try {
            key = GeradorChaveRSA.leChavePublica(readFile(publicKeyFile));
        } catch (InvalidKeySpecException e) {
            throw new CriptoException("Erro recuperando chave: " + e.getLocalizedMessage(), e);
        }
        EncriptadorRSA encriptadorRSA = new EncriptadorRSA();
        MensagemEncriptada me = encriptadorRSA.encriptaMensagem(convertStringToBytes(data), key);
        byte[] cript = new byte[me.getMensagemEncriptada().size() * BLOCK_SIZE];
        for (int i = 0; i < me.getMensagemEncriptada().size(); i++) {
            System.arraycopy(me.getMensagemEncriptada().get(i), 0, cript, i * BLOCK_SIZE, BLOCK_SIZE);
        }

        return convertBytesToString(Base64.encodeBase64(cript, true));
    }

    /**
     * Descriptografa os dados utilizando uma chave publica.
     * 
     * @param privateKeyFile a chave pública.
     * @param cript os dados a serem descriptografados.
     * @return os dados descriptografados.
     */
    public static String decripta(String privateKeyFile, String cript) {
        PrivateKey key;
        try {
            key = GeradorChaveRSA.leChavePrivada(readFile(privateKeyFile));
        } catch (InvalidKeySpecException e) {
            throw new CriptoException("Erro recuperando chave: " + e.getLocalizedMessage());
        }
        return decripta(key, cript);
    }

    /**
     * Descriptografa os dados utilizando uma chave publica.
     * 
     * @param key a chave pública.
     * @param cript os dados a serem descriptografados.
     * @return os dados descriptografados.
     */
    public static String decripta(PrivateKey key, String cript) {
        byte[] criptCompleteData = Base64.decodeBase64(convertStringToBytes(cript));

        if (criptCompleteData.length % BLOCK_SIZE != 0) {
            throw new CriptoException("Data size should be a multiple of " + BLOCK_SIZE + ", but is " + criptCompleteData.length);
        }

        List<byte[]> criptBlockedData = new ArrayList<byte[]>();
        for (int i = 0; i < criptCompleteData.length / BLOCK_SIZE; i++) {
            byte[] b = new byte[BLOCK_SIZE];
            System.arraycopy(criptCompleteData, i * BLOCK_SIZE, b, 0, BLOCK_SIZE);
            criptBlockedData.add(b);
        }

        EncriptadorRSA encriptadorRSA = new EncriptadorRSA();
        MensagemEncriptada me = new MensagemEncriptada(criptBlockedData);
        return convertBytesToString(encriptadorRSA.decriptaMensagem(me, key));
    }

    /**
     * Armazena a chave no arquivo com o caminho dado.
     * 
     * @param key key
     * @param path path
     */
    private static void storeKeyIntoFile(Key key, String path) {
        File file = new File(path);
        if (file.isDirectory()) {
            throw new CriptoException("Arquivo dado é um diretório.");
        }
        if (file.exists()) {
            file.delete();
        }
        try {
            IOUtils.copyFullyClose(new ByteArrayInputStream(key.getEncoded()), new FileOutputStream(file));
        } catch (FileNotFoundException e) {
            throw new CriptoException("Arquivo não encontrado.", e);
        } catch (IOException e) {
            throw new CriptoException("Falha de E/S.", e);
        }
    }

    /**
     * Lê o conteúdo do arquivo
     * 
     * @param file file
     * @return byte array
     */
    private static byte[] readFile(String file) {
        try {
            return IOUtils.readFully(new FileInputStream(file));
        } catch (FileNotFoundException e) {
            throw new CriptoException("Arquivo não encontrado.", e);
        } catch (IOException e) {
            throw new CriptoException("Falha de E/S.", e);
        }
    }

    /**
     * Converte um array de bytes em uma string, utilizando o encoding UTF-8
     * 
     * @param bytes bytes
     * @return String
     */
    private static String convertBytesToString(byte[] bytes) {
        try {
            return new String(bytes, UTF_8);
        } catch (UnsupportedEncodingException e) {
            throw new CriptoException("Codificação não suportada.", e);
        }
    }

    /**
     * Converte uma string em um array de bytes, utilizando o encoding UTF-8
     * 
     * @param string string
     * @return byta array
     */
    private static byte[] convertStringToBytes(String string) {
        try {
            return string.getBytes(UTF_8);
        } catch (UnsupportedEncodingException e) {
            throw new CriptoException("Codificação não suportada.", e);
        }
    }
}
